/* Copyright 2015 The jeo project. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.jeo.geobuf; import com.vividsolutions.jts.geom.CoordinateSequence; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.geom.LinearRing; import com.vividsolutions.jts.geom.MultiLineString; import com.vividsolutions.jts.geom.MultiPoint; import com.vividsolutions.jts.geom.MultiPolygon; import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Polygon; import com.vividsolutions.jts.geom.impl.PackedCoordinateSequenceFactory; import io.jeo.data.Disposable; import io.jeo.geobuf.Geobuf.Data; import io.jeo.geobuf.Geobuf.Data.DataTypeCase; import io.jeo.geobuf.Geobuf.Data.Feature; import io.jeo.geobuf.Geobuf.Data.Feature.IdTypeCase; import io.jeo.geobuf.Geobuf.Data.FeatureCollection; import io.jeo.geobuf.Geobuf.Data.Geometry; import io.jeo.geobuf.Geobuf.Data.Value; import io.jeo.geobuf.Geobuf.Data.Value.ValueTypeCase; import io.jeo.proj.Proj; import io.jeo.vector.FeatureCursor; import io.jeo.vector.MapFeature; import org.osgeo.proj4j.CoordinateReferenceSystem; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Reads a geobuf protocol buffer stream. */ public class GeobufReader implements Disposable { static Logger LOG = LoggerFactory.getLogger(GeobufReader.class); // raw input final InputStream in; // the geobuf payload final Data.Builder data; // property keys final List<String> keys; // coordinate dimensions final int dim; // precision ^ 10 final double e; // coordinate / geometry factories final PackedCoordinateSequenceFactory csFactory; final GeometryFactory gFactory; public GeobufReader(InputStream in) throws IOException { this.in = in; data = Data.newBuilder(); data.mergeFrom(in); keys = data.getKeysList(); dim = data.getDimensions(); e = Math.pow(10, data.getPrecision()); csFactory = new PackedCoordinateSequenceFactory(PackedCoordinateSequenceFactory.DOUBLE, dim); gFactory = new GeometryFactory(csFactory); } public com.vividsolutions.jts.geom.Geometry geometry() { DataTypeCase type = data.getDataTypeCase(); if (type != DataTypeCase.GEOMETRY) { throw new IllegalArgumentException("Geobuf data type: " + type + ", not geometry"); } return decode(data.getGeometry()); } public Point point() { return (Point) geometry(); } public LineString lineString() { return (LineString) geometry(); } public Polygon polygon() { return (Polygon) geometry(); } public MultiPoint multiPoint() { return (MultiPoint) geometry(); } public MultiLineString multiLineString() { return (MultiLineString) geometry(); } public MultiPolygon multiPolygon() { return (MultiPolygon) geometry(); } public io.jeo.vector.Feature feature() { DataTypeCase type = data.getDataTypeCase(); if (type != DataTypeCase.FEATURE) { throw new IllegalArgumentException("Geobuf data type: " + type + ", not feature"); } return decode(data.getFeature()); } public FeatureCursor featureCollection() { DataTypeCase type = data.getDataTypeCase(); if (type != DataTypeCase.FEATURE_COLLECTION) { throw new IllegalArgumentException("Geobuf data type: " + type + ", not feature collection"); } return decode(data.getFeatureCollection()); } public CoordinateReferenceSystem crs() { final List<Integer> props; final List<Value> vals; switch(data.getDataTypeCase()) { case GEOMETRY: props = data.getGeometry().getCustomPropertiesList(); vals = data.getGeometry().getValuesList(); break; case FEATURE: props = data.getFeature().getCustomPropertiesList(); vals = data.getFeature().getValuesList(); break; case FEATURE_COLLECTION: props = data.getFeatureCollection().getCustomPropertiesList(); vals = data.getFeatureCollection().getValuesList(); break; default: return null; } int i = data.getKeysList().indexOf(CustomKeys.CRS); int p = props.indexOf(i); if (p > -1 && p % 2 == 0) { return Proj.crs((String) decode(vals.get(props.get(p+1)))); } return null; } com.vividsolutions.jts.geom.Geometry decode(Geometry g) { switch (g.getType()) { case POINT: return decodePoint(g); case LINESTRING: return decodeLine(g); case POLYGON: return decodePolygon(g); case MULTIPOINT: return decodeMultiPoint(g); case MULTILINESTRING: return decodeMultiLine(g); case MULTIPOLYGON: return decodeMultiPolygon(g); case GEOMETRYCOLLECTION: return decodeCollection(g); default: throw new UnsupportedOperationException(); } } com.vividsolutions.jts.geom.Point decodePoint(Geometry g) { double[] p = new double[dim]; for (int k = 0; k < dim; k++) { p[k] = g.getCoords(k) / e; } return gFactory.createPoint(csFactory.create(p, dim)); } com.vividsolutions.jts.geom.LineString decodeLine(Geometry g) { return gFactory.createLineString(readAllCoords(g, false)); } com.vividsolutions.jts.geom.LineString decodeLine(Geometry g, int start, int len) { return gFactory.createLineString(readCoords(g, start, len, false)); } com.vividsolutions.jts.geom.LinearRing decodeRing(Geometry g, int start, int len) { return gFactory.createLinearRing(readCoords(g, start, len, true)); } com.vividsolutions.jts.geom.Polygon decodePolygon(Geometry g) { if (g.getLengthsCount() > 0) { return decodePolygon(g, 0, 0, g.getLengthsCount()); } else { return gFactory.createPolygon(readAllCoords(g, true)); } } com.vividsolutions.jts.geom.Polygon decodePolygon(Geometry g, int start, int lenStart, int nRings) { int len = g.getLengths(lenStart); LinearRing shell = decodeRing(g, start, len); LinearRing[] holes = new LinearRing[nRings-1]; for (int i = 0; i < holes.length; i++) { start += len; len = g.getLengths(lenStart+i+1); holes[i] = decodeRing(g, start, len); } return gFactory.createPolygon(shell, holes); } com.vividsolutions.jts.geom.MultiPoint decodeMultiPoint(Geometry g) { return gFactory.createMultiPoint(readCoords(g, 0, g.getCoordsCount()/dim, false)); } com.vividsolutions.jts.geom.MultiLineString decodeMultiLine(Geometry g) { LineString[] lines; if (g.getLengthsCount() > 0) { lines = new LineString[g.getLengthsCount()]; int start = 0; for (int i = 0; i < lines.length; i++) { int len = g.getLengths(i); lines[i] = decodeLine(g, start, len); start += len; } } else { lines = new LineString[]{ decodeLine(g) }; } return gFactory.createMultiLineString(lines); } com.vividsolutions.jts.geom.MultiPolygon decodeMultiPolygon(Geometry g) { Polygon[] polygons; if (g.getLengthsCount() > 0) { List<Polygon> list = new ArrayList<>(g.getLengths(0)); int start = 0; int i = 1; while (i < g.getLengthsCount()) { int nrings = g.getLengths(i++); list.add(decodePolygon(g, start, i, nrings)); for (int j = 0; j < nrings; j++) { start += g.getLengths(i); i++; } } polygons = list.toArray(new Polygon[list.size()]); } else { polygons = new Polygon[]{decodePolygon(g)}; } return gFactory.createMultiPolygon(polygons); } com.vividsolutions.jts.geom.Geometry decodeCollection(Geometry g) { List<com.vividsolutions.jts.geom.Geometry> geoms = new ArrayList<>(); if (g.getGeometriesCount() < 2) { geoms.add(decode(g)); } else { for (int i = 0; i < g.getGeometriesCount(); i++) { geoms.add(decode(g.getGeometries(i))); } } return gFactory.buildGeometry(geoms); } io.jeo.vector.Feature decode(Feature f) { Map<String,Object> values = new LinkedHashMap<>(); // geometry values.put("geometry", decode(f.getGeometry())); // properties for (int i = 0; i < f.getPropertiesCount(); i += 2) { int key = f.getProperties(i); int val = f.getProperties(i+1); values.put(keys.get(key), decode(f.getValues(val))); } // id String id = f.getIdTypeCase() == IdTypeCase.INT_ID ? String.valueOf(f.getIntId()) : f.getId(); return new MapFeature(id, values); } Object decode(Value val) { ValueTypeCase t = val.getValueTypeCase(); switch(t) { case STRING_VALUE: return val.getStringValue(); case DOUBLE_VALUE: return val.getDoubleValue(); case POS_INT_VALUE: return val.getPosIntValue(); case NEG_INT_VALUE: return val.getNegIntValue(); case BOOL_VALUE: return val.getBoolValue(); case JSON_VALUE: return val.getJsonValue(); case VALUETYPE_NOT_SET: return null; default: throw new UnsupportedOperationException("Unsupported value type: " + t); } } FeatureCursor decode(FeatureCollection fcol) { return new GeobufCursor(fcol, this); } CoordinateSequence readCoords(Geometry g, int start, int len, boolean close) { double[] coords = new double[dim*(len + (close?1:0))]; long[] coord = new long[dim]; for (int i = start; i < start+len; i++) { int j = i*dim; for (int k = 0; k < dim; k++) { coord[k] += g.getCoords(j+k); coords[(i-start)*dim+k] = coord[k] / e; } } if (close) { System.arraycopy(coords, 0, coords, coords.length-dim, dim); } return csFactory.create(coords, dim); } CoordinateSequence readAllCoords(Geometry g, boolean close) { return readCoords(g, 0, g.getCoordsCount()/dim, close); } @Override public void close() { try { in.close(); } catch (IOException e) { LOG.debug("Error closing geobuf reader", e); } } }